Merge pull request #777 from cantino/js_agent_can_be_dryrun

Allow JavaScriptAgent to dry run and have inline syntax highlighting

Andrew Cantino 9 years ago
parent
commit
79c89ee115

+ 1 - 1
Gemfile

@@ -86,7 +86,7 @@ gem 'sass-rails',   '~> 5.0'
86 86
 gem 'select2-rails', '~> 3.5.4'
87 87
 gem 'spectrum-rails'
88 88
 gem 'string-scrub'	# for ruby <2.1
89
-gem 'therubyracer', '~> 0.12.1'
89
+gem 'therubyracer', '~> 0.12.2'
90 90
 gem 'typhoeus', '~> 0.6.3'
91 91
 gem 'uglifier', '>= 1.3.0'
92 92
 

+ 2 - 2
Gemfile.lock

@@ -427,7 +427,7 @@ GEM
427 427
     systemu (2.6.4)
428 428
     term-ansicolor (1.3.0)
429 429
       tins (~> 1.0)
430
-    therubyracer (0.12.1)
430
+    therubyracer (0.12.2)
431 431
       libv8 (~> 3.16.14.0)
432 432
       ref
433 433
     thor (0.19.1)
@@ -565,7 +565,7 @@ DEPENDENCIES
565 565
   spring (~> 1.3.0)
566 566
   spring-commands-rspec
567 567
   string-scrub
568
-  therubyracer (~> 0.12.1)
568
+  therubyracer (~> 0.12.2)
569 569
   tumblr_client
570 570
   twilio-ruby (~> 3.11.5)
571 571
   twitter (~> 5.14.0)

+ 4 - 0
app/assets/javascripts/ace.js.coffee

@@ -0,0 +1,4 @@
1
+#= require ace/ace
2
+#= require ace/mode-javascript.js
3
+#= require ace/mode-markdown.js
4
+#= require ace/mode-coffee.js

+ 35 - 2
app/assets/javascripts/pages/agent-edit-page.js.coffee

@@ -2,6 +2,7 @@ class @AgentEditPage
2 2
   constructor: ->
3 3
     $("#agent_source_ids").on "change", @showEventDescriptions
4 4
     @showCorrectRegionsOnStartup()
5
+    $("form.agent-form").on "submit", => @updateFromEditors()
5 6
 
6 7
     $("#agent_name").each ->
7 8
       # Select the number suffix if this is a cloned agent.
@@ -17,6 +18,7 @@ class @AgentEditPage
17 18
       @handleTypeChange(true)
18 19
     else
19 20
       @enableDryRunButton()
21
+      @buildAce()
20 22
 
21 23
   handleTypeChange: (firstTime) ->
22 24
     $(".event-descriptions").html("").hide()
@@ -61,6 +63,7 @@ class @AgentEditPage
61 63
           window.jsonEditor = setupJsonEditor()[0]
62 64
 
63 65
         @enableDryRunButton()
66
+        @buildAce()
64 67
 
65 68
         window.initializeFormCompletable()
66 69
 
@@ -134,15 +137,45 @@ class @AgentEditPage
134 137
       else
135 138
         @hideEventCreation()
136 139
 
140
+  buildAce: ->
141
+    $(".ace-editor").each ->
142
+      unless $(this).data('initialized')
143
+        $(this).data('initialized', true)
144
+        $source = $($(this).data('source')).hide()
145
+        editor = ace.edit(this)
146
+        $(this).data('ace-editor', editor)
147
+        session = editor.getSession()
148
+        session.setTabSize(2)
149
+        session.setUseSoftTabs(true)
150
+        session.setUseWrapMode(false)
151
+        editor.setTheme("ace/theme/chrome")
152
+
153
+        setSyntax = ->
154
+          switch $("[name='agent[options][language]']").val()
155
+            when 'JavaScript' then session.setMode("ace/mode/javascript")
156
+            when 'CoffeeScript' then session.setMode("ace/mode/coffee")
157
+            else session.setMode("ace/mode/text")
158
+
159
+        $("[name='agent[options][language]']").on 'change', setSyntax
160
+        setSyntax()
161
+
162
+        session.setValue($source.val())
163
+
164
+  updateFromEditors: ->
165
+    $(".ace-editor").each ->
166
+      $source = $($(this).data('source'))
167
+      $source.val($(this).data('ace-editor').getSession().getValue())
168
+
137 169
   enableDryRunButton: ->
138 170
     $(".agent-dry-run-button").prop('disabled', false).off().on "click", @invokeDryRun
139 171
 
140 172
   disableDryRunButton: ->
141 173
     $(".agent-dry-run-button").prop('disabled', true)
142 174
 
143
-  invokeDryRun: (e) ->
175
+  invokeDryRun: (e) =>
144 176
     e.preventDefault()
145
-    Utils.handleDryRunButton(this)
177
+    @updateFromEditors()
178
+    Utils.handleDryRunButton(e.target)
146 179
 
147 180
 $ ->
148 181
   Utils.registerPage(AgentEditPage, forPathsMatching: /^agents/)

+ 26 - 0
app/assets/javascripts/pages/user-credential-page.js.coffee

@@ -0,0 +1,26 @@
1
+class @UserCredentialPage
2
+  constructor: ->
3
+    editor = ace.edit("ace-credential-value")
4
+    editor.getSession().setTabSize(2)
5
+    editor.getSession().setUseSoftTabs(true)
6
+    editor.getSession().setUseWrapMode(false)
7
+    editor.setTheme("ace/theme/chrome")
8
+
9
+    setMode = ->
10
+      mode = $("#user_credential_mode").val()
11
+      if mode == 'java_script'
12
+        editor.getSession().setMode("ace/mode/javascript")
13
+      else
14
+        editor.getSession().setMode("ace/mode/text")
15
+
16
+    setMode()
17
+    $("#user_credential_mode").on 'change', setMode
18
+
19
+    $textarea = $('#user_credential_credential_value').hide()
20
+    editor.getSession().setValue($textarea.val())
21
+
22
+    $textarea.closest('form').on 'submit', ->
23
+      $textarea.val(editor.getSession().getValue())
24
+
25
+$ ->
26
+  Utils.registerPage(UserCredentialPage, forPathsMatching: /^user_credentials\/\d+/)

+ 0 - 29
app/assets/javascripts/user_credentials.js.coffee

@@ -1,29 +0,0 @@
1
-#= require ace/ace
2
-#= require ace/mode-javascript.js
3
-#= require ace/mode-markdown.js
4
-#= require_self
5
-
6
-# This is not included in the core application.js bundle.
7
-
8
-$ ->
9
-  editor = ace.edit("ace-credential-value")
10
-  editor.getSession().setTabSize(2)
11
-  editor.getSession().setUseSoftTabs(true)
12
-  editor.getSession().setUseWrapMode(false)
13
-  editor.setTheme("ace/theme/chrome")
14
-
15
-  setMode = ->
16
-    mode = $("#user_credential_mode").val()
17
-    if mode == 'java_script'
18
-      editor.getSession().setMode("ace/mode/javascript")
19
-    else
20
-      editor.getSession().setMode("ace/mode/text")
21
-
22
-  setMode()
23
-  $("#user_credential_mode").on 'change', setMode
24
-
25
-  $textarea = $('#user_credential_credential_value').hide()
26
-  editor.getSession().setValue($textarea.val())
27
-
28
-  $textarea.closest('form').on 'submit', ->
29
-    $textarea.val(editor.getSession().getValue())

+ 8 - 2
app/assets/stylesheets/application.css.scss.erb

@@ -179,12 +179,18 @@ span.not-applicable:after {
179 179
   cursor: pointer;
180 180
 }
181 181
 
182
-// Credentials
182
+// Credentials and Ace Editor
183 183
 
184 184
 #ace-credential-value {
185 185
   position: relative;
186 186
   width: 940px;
187
-  height: 400px;
187
+  height: 300px;
188
+}
189
+
190
+.ace-editor {
191
+  position: relative;
192
+  width: 550px;
193
+  height: 300px;
188 194
 }
189 195
 
190 196
 // Disabled

+ 1 - 0
app/concerns/dry_runnable.rb

@@ -60,6 +60,7 @@ module DryRunnable
60 60
     def create_event(event_hash)
61 61
       if can_create_events?
62 62
         @dry_run_results[:events] << event_hash[:payload]
63
+        events.build({ user: user, expires_at: new_event_expiration_date }.merge(event_hash))
63 64
       else
64 65
         error "This Agent cannot create events!"
65 66
       end

+ 1 - 1
app/concerns/form_configurable.rb

@@ -32,7 +32,7 @@ module FormConfigurable
32 32
       options = args.extract_options!.reverse_merge(roles: [], type: :string)
33 33
 
34 34
       if args.all? { |arg| arg.is_a?(Symbol) }
35
-        options.assert_valid_keys([:type, :roles, :values])
35
+        options.assert_valid_keys([:type, :roles, :values, :ace])
36 36
       end
37 37
 
38 38
       if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array))

+ 6 - 0
app/helpers/application_helper.rb

@@ -86,6 +86,12 @@ module ApplicationHelper
86 86
     ].join.html_safe, class: "label label-default label-service service-#{service.provider}"
87 87
   end
88 88
 
89
+  def load_ace_editor!
90
+    unless content_for?(:ace_editor_script)
91
+      content_for :ace_editor_script, javascript_include_tag('ace')
92
+    end
93
+  end
94
+
89 95
   def highlighted?(id)
90 96
     @highlighted_ranges ||=
91 97
       case value = params[:hl].presence

+ 24 - 6
app/models/agents/java_script_agent.rb

@@ -3,6 +3,10 @@ require 'cgi'
3 3
 
4 4
 module Agents
5 5
   class JavaScriptAgent < Agent
6
+    include FormConfigurable
7
+
8
+    can_dry_run!
9
+
6 10
     default_schedule "never"
7 11
 
8 12
     description <<-MD
@@ -25,6 +29,11 @@ module Agents
25 29
       * `this.unescapeHtml(htmlToUnescape)`
26 30
     MD
27 31
 
32
+    form_configurable :language, type: :array, values: %w[JavaScript CoffeeScript]
33
+    form_configurable :code, type: :text, ace: true
34
+    form_configurable :expected_receive_period_in_days
35
+    form_configurable :expected_update_period_in_days
36
+
28 37
     def validate_options
29 38
       cred_name = credential_referenced_by_code
30 39
       if cred_name
@@ -32,6 +41,10 @@ module Agents
32 41
       else
33 42
         errors.add(:base, "The 'code' option is required") unless options['code'].present?
34 43
       end
44
+
45
+      if interpolated['language'].present? && !interpolated['language'].downcase.in?(%w[javascript coffeescript])
46
+        errors.add(:base, "The 'language' must be JavaScript or CoffeeScript")
47
+      end
35 48
     end
36 49
 
37 50
     def working?
@@ -69,7 +82,7 @@ module Agents
69 82
             this.memory('callCount', callCount + 1);
70 83
           }
71 84
         };
72
-        
85
+
73 86
         Agent.receive = function() {
74 87
           var events = this.incomingEvents();
75 88
           for(var i = 0; i < events.length; i++) {
@@ -79,9 +92,10 @@ module Agents
79 92
       JS
80 93
 
81 94
       {
82
-        "code" => js_code.gsub(/[\n\r\t]/, '').strip,
83
-        'expected_receive_period_in_days' => "2",
84
-        'expected_update_period_in_days' => "2"
95
+        'code' => Utils.unindent(js_code),
96
+        'language' => 'JavaScript',
97
+        'expected_receive_period_in_days' => '2',
98
+        'expected_update_period_in_days' => '2'
85 99
       }
86 100
     end
87 101
 
@@ -107,7 +121,11 @@ module Agents
107 121
       context["escapeHtml"] = lambda { |a, x| CGI.escapeHTML(x) }
108 122
       context["unescapeHtml"] = lambda { |a, x| CGI.unescapeHTML(x) }
109 123
 
110
-      context.eval(code)
124
+      if (options['language'] || '').downcase == 'coffeescript'
125
+        context.eval(CoffeeScript.compile code)
126
+      else
127
+        context.eval(code)
128
+      end
111 129
       context.eval("Agent.#{js_function}();")
112 130
     end
113 131
 
@@ -121,7 +139,7 @@ module Agents
121 139
     end
122 140
 
123 141
     def credential_referenced_by_code
124
-      interpolated['code'] =~ /\Acredential:(.*)\Z/ && $1
142
+      (interpolated['code'] || '').strip =~ /\Acredential:(.*)\Z/ && $1
125 143
     end
126 144
 
127 145
     def setup_javascript

+ 1 - 1
app/models/agents/website_agent.rb

@@ -379,7 +379,7 @@ module Agents
379 379
       end
380 380
     end
381 381
 
382
-    # Wraps Faraday::Utilsa::Headers
382
+    # Wraps Faraday::Utils::Headers
383 383
     class HeaderDrop < LiquidDroppable::Drop
384 384
       def before_method(name)
385 385
         @object[name.tr('_', '-')]

+ 6 - 1
app/presenters/form_configurable_agent_presenter.rb

@@ -20,7 +20,12 @@ class FormConfigurableAgentPresenter < Decorator
20 20
 
21 21
     case data[:type]
22 22
     when :text
23
-      @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3)
23
+      @view.content_tag 'div' do
24
+        @view.concat @view.text_area_tag("agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3))
25
+        if data[:ace].present?
26
+          @view.concat @view.content_tag('div', '', class: 'ace-editor', data: { source: "[name='agent[options][#{attribute}]']" })
27
+        end
28
+      end
24 29
     when :boolean
25 30
       @view.content_tag 'div' do
26 31
         @view.concat(@view.content_tag('label', class: 'radio-inline') do

+ 6 - 3
app/views/agents/_form.html.erb

@@ -1,3 +1,5 @@
1
+<% load_ace_editor! %>
2
+
1 3
 <% if @agent.errors.any? %>
2 4
   <div class="row well model-errors">
3 5
     <h2><%= pluralize(@agent.errors.count, "error") %> prohibited this Agent from being saved:</h2>
@@ -8,9 +10,10 @@
8 10
 <% end %>
9 11
 
10 12
 <%= form_for(@agent,
11
-             :as => :agent,
12
-             :url => @agent.new_record? ? agents_path : agent_path(@agent),
13
-             :method => @agent.new_record? ? "POST" : "PUT") do |f| %>
13
+             as: :agent,
14
+             url: @agent.new_record? ? agents_path : agent_path(@agent),
15
+             method: @agent.new_record? ? "POST" : "PUT",
16
+             html: { class: 'agent-form' }) do |f| %>
14 17
 
15 18
   <div class="row">
16 19
     <div class="col-md-6">

+ 1 - 0
app/views/layouts/application.html.erb

@@ -9,6 +9,7 @@
9 9
     <%= stylesheet_link_tag    "application", :media => "all" %>
10 10
     <%= javascript_include_tag "application" %>
11 11
     <%= csrf_meta_tags %>
12
+    <%= yield(:ace_editor_script) %>
12 13
     <%= yield(:head) %>
13 14
   </head>
14 15
   <body>

+ 2 - 2
app/views/user_credentials/_form.html.erb

@@ -1,3 +1,5 @@
1
+<% load_ace_editor! %>
2
+
1 3
 <%= form_for(@user_credential, :method => @user_credential.new_record? ? "POST" : "PUT") do |f| %>
2 4
   <% if @user_credential.errors.any? %>
3 5
     <div class="row well">
@@ -40,5 +42,3 @@
40 42
     </div>
41 43
   </div>
42 44
 <% end %>
43
-
44
-<%= javascript_include_tag "user_credentials" %>

+ 2 - 0
bin/threaded.rb

@@ -2,6 +2,8 @@ require 'thread'
2 2
 require 'huginn_scheduler'
3 3
 require 'twitter_stream'
4 4
 
5
+Rails.configuration.cache_classes = true
6
+
5 7
 STDOUT.sync = true
6 8
 STDERR.sync = true
7 9
 

+ 1 - 1
config/environments/production.rb

@@ -63,7 +63,7 @@ Huginn::Application.configure do
63 63
   end
64 64
 
65 65
   # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added)
66
-  config.assets.precompile += %w( diagram.js graphing.js map_marker.js user_credentials.js )
66
+  config.assets.precompile += %w( diagram.js graphing.js map_marker.js ace.js )
67 67
 
68 68
   # Ignore bad email addresses and do not raise email delivery errors.
69 69
   # Set this to true and configure the email server for immediate delivery to raise delivery errors.

+ 28 - 2
spec/models/agents/java_script_agent_spec.rb

@@ -23,6 +23,21 @@ describe Agents::JavaScriptAgent do
23 23
       expect(@agent).not_to be_valid
24 24
     end
25 25
 
26
+    it "checks for a valid 'language', but allows nil" do
27
+      expect(@agent).to be_valid
28
+      @agent.options['language'] = ''
29
+      expect(@agent).to be_valid
30
+      @agent.options.delete('language')
31
+      expect(@agent).to be_valid
32
+      @agent.options['language'] = 'foo'
33
+      expect(@agent).not_to be_valid
34
+
35
+      %w[javascript JavaScript coffeescript CoffeeScript].each do |valid_language|
36
+        @agent.options['language'] = valid_language
37
+        expect(@agent).to be_valid
38
+      end
39
+    end
40
+
26 41
     it "accepts a credential, but it must exist" do
27 42
       expect(@agent).to be_valid
28 43
       @agent.options['code'] = 'credential:foo'
@@ -74,11 +89,10 @@ describe Agents::JavaScriptAgent do
74 89
       }.to change { Event.count }.by(2)
75 90
     end
76 91
 
77
-
78 92
     describe "using credentials as code" do
79 93
       before do
80 94
         @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };'
81
-        @agent.options['code'] = 'credential:code-foo'
95
+        @agent.options['code'] = "credential:code-foo\n\n"
82 96
         @agent.save!
83 97
       end
84 98
 
@@ -238,5 +252,17 @@ describe Agents::JavaScriptAgent do
238 252
         }.not_to change { Event.count }
239 253
       end
240 254
     end
255
+
256
+    describe "using CoffeeScript" do
257
+      it "will accept a 'language' of 'CoffeeScript'" do
258
+        @agent.options['code'] = 'Agent.check = -> this.log("hello from coffeescript")'
259
+        @agent.options['language'] = 'CoffeeScript'
260
+        @agent.save!
261
+        expect {
262
+          @agent.check
263
+        }.not_to raise_error
264
+        expect(AgentLog.last.message).to eq("hello from coffeescript")
265
+      end
266
+    end
241 267
   end
242 268
 end